﻿namespace LMT.Communication.Tcp
{
    using System.Collections.Generic;
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using DotNet = System.Net.Sockets;

    /// <summary>
    /// 
    /// </summary>
    public class TcpServer
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="client">The client.</param>
        public delegate void ClientConnectHandler(DotNet.TcpClient client);

        /// <summary>
        /// 
        /// </summary>
        /// <param name="client">The client.</param>
        /// <param name="data">The data.</param>
        public delegate void ReceiveDataHandler(DotNet.TcpClient client, byte[] data);

        /// <summary>
        /// Occurs when [client connected].
        /// </summary>
        public event ClientConnectHandler ClientConnected;

        /// <summary>
        /// Occurs when [client disconnect].
        /// </summary>
        public event ClientConnectHandler ClientDisconnect;

        /// <summary>
        /// Occurs when [receive data event].
        /// </summary>
        public event ReceiveDataHandler ReceiveDataEvent;

        /// <summary>
        /// 
        /// </summary>
        private TcpListener serverListen;

        /// <summary>
        /// 
        /// </summary>
        private int port;

        /// <summary>
        /// 
        /// </summary>
        private Dictionary<string, DotNet.TcpClient> clientList;

        /// <summary>
        /// 
        /// </summary>
        private bool isStarting;

        /// <summary>
        /// Initializes a new instance of the <see cref="TcpServer"/> class.
        /// </summary>
        /// <param name="ipAddr">The ip addr.</param>
        /// <param name="port">The port.</param>
        /// <param name="bufferSize">Size of the buffer.</param>
        public TcpServer(string ipAddr, int port, int? bufferSize)
        {
            if (TcpHelper.Instance == null)
            {
                TcpHelper.CreateIntance(bufferSize);
            }

            IPAddress localAddr = IPAddress.Parse(ipAddr);
            this.port = port;

            this.serverListen = new TcpListener(localAddr, this.port);
            this.serverListen.Server.ReceiveBufferSize = TcpHelper.Instance.SizeOfBlockBuffer;
            this.serverListen.Server.SendBufferSize = TcpHelper.Instance.SizeOfBlockBuffer;

            this.clientList = new Dictionary<string, DotNet.TcpClient>();
        }

        /// <summary>
        /// Begins the listenning.
        /// </summary>
        public void Start()
        {
            this.serverListen.Start();

            this.isStarting = true;
            Thread waitforClient = new Thread(delegate()
            {
                this.InternalThreadWaitingForClient(this.serverListen);
            });

            waitforClient.IsBackground = true;
            waitforClient.Start();
        }

        /// <summary>
        /// Stops this instance.
        /// </summary>
        public void Stop()
        {
            this.isStarting = false;

            lock (this.clientList)
            {
                this.clientList.Clear();
            }

            this.serverListen.Stop();
        }

        /// <summary>
        /// Sends the data to all active clients.
        /// </summary>
        /// <param name="data">The data.</param>
        public void Send(byte[] data)
        {
            lock (this.clientList)
            {
                foreach (string clientKey in this.clientList.Keys)
                {
                    if (this.clientList[clientKey].Connected)
                    {
                        TcpHelper.Instance.SendData(this.clientList[clientKey].GetStream(), data);
                    }
                }
            }
        }

        /// <summary>
        /// Sends the data to specific client.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="tcpClient">The TCP client.</param>
        public void Send(byte[] data, DotNet.TcpClient tcpClient)
        {
            TcpHelper.Instance.SendData(tcpClient.GetStream(), data);
        }

        /// <summary>
        /// Internals the thread waiting for client.
        /// </summary>
        /// <param name="server">The server.</param>
        private void InternalThreadWaitingForClient(TcpListener server)
        {
            try
            {
                DotNet.TcpClient client = null;

                while (this.isStarting)
                {
                    client = server.AcceptTcpClient();

                    if (client.Connected)
                    {
                        int result = this.AddClientToList(client);

                        Thread doReceiveClient = new Thread(delegate()
                        {
                            // if result = 0, it means reconnect
                            if (result == 1 && this.ClientConnected != null)
                            {
                                this.ClientConnected(client);
                            }

                            this.ReceivingFromClient(client, new Queue<byte[]>(10));
                        });

                        doReceiveClient.IsBackground = true;
                        doReceiveClient.Start();
                    }
                }
            }
            catch
            {
                
            }
        }

        /// <summary>
        /// Workings the with client.
        /// </summary>
        /// <param name="tcpClient">The TCP client.</param>
        /// <param name="queueData">The receive data.</param>
        /// <param name="waitMsg">The wait MSG.</param>
        private void ReceivingFromClient(DotNet.TcpClient tcpClient, Queue<byte[]> queueData)
        {
            AutoResetEvent waitMsg = null;
            Thread processMsg = null;

            if (this.ReceiveDataEvent != null)
            {
                waitMsg = new AutoResetEvent(false);
                processMsg = new Thread(delegate() { this.ProcessClientMessage(tcpClient, queueData, waitMsg); });
                processMsg.Start();
            }

            try
            {
                while (this.isStarting && tcpClient.Connected)
                {
                    byte[] data = TcpHelper.Instance.ReadData(tcpClient.GetStream());

                    if (this.ReceiveDataEvent != null)
                    {
                        queueData.Enqueue(data);
                        waitMsg.Set();
                    }

                    if (data == null)
                    {
                        if (!this.IsConnected(tcpClient))
                        {
                            break;
                        }
                    }
                }

                this.RemoveClientFromList(tcpClient);
            }
            catch (SocketException se)
            {
                if (se.ErrorCode == 10054)
                {
                    this.RemoveClientFromList(tcpClient);
                }
            }
            catch (IOException ex)
            {
                if (ex.InnerException != null)
                {
                    SocketException se = ex.InnerException as SocketException;
                    if (se != null)
                    {
                        if (se.ErrorCode == 10054)
                        {
                            this.RemoveClientFromList(tcpClient);
                        }
                    }
                }
            }
            finally
            {
                if (waitMsg != null)
                {
                    waitMsg.Set();
                    processMsg.Join();
                    waitMsg.Close();
                    waitMsg = null;
                }

                if (queueData != null)
                {
                    queueData.Clear();
                    queueData = null;
                }

                if (this.ClientDisconnect != null)
                {
                    this.ClientDisconnect(tcpClient);
                }
            }
        }

        /// <summary>
        /// Processes the client message.
        /// </summary>
        /// <param name="tcpClient">The TCP client.</param>
        /// <param name="receiveData">The receive data.</param>
        private void ProcessClientMessage(DotNet.TcpClient tcpClient, Queue<byte[]> receiveData, AutoResetEvent waitMsg)
        {
            do
            {
                if (receiveData.Count < 1)
                {
                    waitMsg.WaitOne();
                }

                if (tcpClient.Connected && this.ReceiveDataEvent != null)
                {
                    if (receiveData.Count > 0)
                    {
                        byte[] dataDeQueue = receiveData.Dequeue();
                        if (dataDeQueue != null)
                        {
                            this.ReceiveDataEvent(tcpClient, dataDeQueue);
                        }
                        else
                        {
                            break;
                        }
                    }
                }
            }
            while (tcpClient.Connected);
        }

        /// <summary>
        /// Adds the client to list.
        /// </summary>
        /// <param name="tcpClient">The TCP client.</param>
        /// <returns></returns>
        private int AddClientToList(DotNet.TcpClient tcpClient)
        {
            int addResult = 0;

            lock (this.clientList)
            {
                if (this.clientList.ContainsKey(tcpClient.Client.RemoteEndPoint.ToString()))
                {
                    this.clientList[tcpClient.Client.RemoteEndPoint.ToString()] = tcpClient;
                    addResult = 0;
                }
                else
                {
                    this.clientList.Add(tcpClient.Client.RemoteEndPoint.ToString(), tcpClient);
                    addResult = 1;
                }
            }

            return addResult;
        }

        /// <summary>
        /// Removes the client from list.
        /// </summary>
        /// <param name="tcpClient">The TCP client.</param>
        private void RemoveClientFromList(DotNet.TcpClient tcpClient)
        {
            lock (this.clientList)
            {
                this.clientList.Remove(tcpClient.Client.RemoteEndPoint.ToString());
            }
        }

        /// <summary>
        /// Checks the connection state
        /// </summary>
        /// <param name="client">The client.</param>
        /// <returns>
        /// True on connected. False on disconnected.
        /// </returns>
        private bool IsConnected(DotNet.TcpClient client)
        {
            if (client.Client.Connected)
            {
                if ((client.Client.Poll(0, SelectMode.SelectWrite)) && (!client.Client.Poll(0, SelectMode.SelectError)))
                {
                    byte[] buffer = new byte[1];
                    if (client.Client.Receive(buffer, SocketFlags.Peek) == 0)
                    {
                        return false;
                    }
                    else
                    {
                        return true;
                    }
                }
                else
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
        }
    }
}